package nl.unimaas.games.tron.experiment;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.FilenameFilter;
import java.io.ObjectInput;
import java.io.ObjectInputStream;
import java.io.ObjectOutput;
import java.io.ObjectOutputStream;
import java.io.Serializable;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Set;

import nl.unimaas.games.tron.engine.Board;
import nl.unimaas.games.tron.engine.Game;
import nl.unimaas.games.tron.engine.GameListener;
import nl.unimaas.games.tron.player.Player;

public class GameAnalyzer implements GameListener {
	private final static int CATEGORY_PASSIVE = 1;
	private final static int CATEGORY_OFFENSIVE = 2;
	private final static int CATEGORY_DEFENSIVE = 4;
	private final static int CATEGORY_TERRITORIAL = 8;
	private final static int CATEGORY_OBSTRUCTIVE = 16;
	private final static int CATEGORY_RECKLESS = 32;
	private final static int CATEGORY_ALL = 1024;
	
	public static interface MoveCategory {
		public boolean belongsTo(Board b, int playerNr, int m);
		public int getCategory();
	}
	
	public final static MoveCategory ALL = new MoveCategory() {		
		@Override
		public boolean belongsTo(Board b, int playerNr, int m) {
			return true;
		}

		@Override
		public int getCategory() {
			return CATEGORY_ALL;
		}
		
		@Override
		public String toString() {
			return "All";
		}
	};
	
	public final static MoveCategory PASSIVE = new MoveCategory() {		
		@Override
		public boolean belongsTo(Board b, int playerNr, int m) {
			int myPos = b.getPlayerPosition(playerNr);
			int myX = Board.posToX(myPos), myY = Board.posToY(myPos);
			//if (!b.isWallhugPosition(myX, myY))
			//	return false;
			
			if (m < 0) {
				if (m == Board.MOVE_LEFT)
					myX--;
				else
					myY--;
			}
			else {
				if (m == Board.MOVE_RIGHT)
					myX++;
				else
					myY++;
			}
			return (b.isWallhugPosition(myX, myY));
		}

		@Override
		public int getCategory() {
			return CATEGORY_PASSIVE;
		}
		
		@Override
		public String toString() {
			return "Passive";
		}
	};
	
	public final static MoveCategory OFFENSIVE = new MoveCategory() {		
		@Override
		public boolean belongsTo(Board b, int playerNr, int m) {
			int myPos = b.getPlayerPosition(playerNr);
			int otherPos = b.getPlayerPosition(1 - playerNr);
			int otherX = Board.posToX(otherPos), otherY = Board.posToY(otherPos);
			int myX = Board.posToX(myPos), myY = Board.posToY(myPos);
			int newX = myX, newY = myY;
			if (m < 0) {
				if (m == Board.MOVE_LEFT)
					newX = myX - 1;
				else
					newY = myY - 1;
			}
			else {
				if (m == Board.MOVE_RIGHT)
					newX = myX + 1;
				else
					newY = myY + 1;
			}
			int dOld = Board.getManhattanDistance(myX, myY, otherX, otherY);
			int dNew = Board.getManhattanDistance(newX, newY, otherX, otherY);
			return (dNew < dOld);
		}

		@Override
		public int getCategory() {
			return CATEGORY_OFFENSIVE;
		}
		
		@Override
		public String toString() {
			return "Offensive";
		}
	};
	
	public final static MoveCategory DEFENSIVE = new MoveCategory() {		
		@Override
		public boolean belongsTo(Board b, int playerNr, int m) {
			return !OFFENSIVE.belongsTo(b, playerNr, m);
		}

		@Override
		public int getCategory() {
			return CATEGORY_DEFENSIVE;
		}
		
		@Override
		public String toString() {
			return "Defensive";
		}
	};
	
	public final static MoveCategory TERRITORIAL = new MoveCategory() {
		@Override
		public boolean belongsTo(Board b, int playerNr, int m) {
			int myPos = b.getPlayerPosition(playerNr);
			int myX = Board.posToX(myPos), myY = Board.posToY(myPos);
			if (b.getFreeCount(myX, myY) <3)
				return false;
			else {
				int newX = myX, newY = myY;
				if (m < 0) {
					if (m == Board.MOVE_LEFT)
						newX = myX - 1;
					else
						newY = myY - 1;
				}
				else {
					if (m == Board.MOVE_RIGHT)
						newX = myX + 1;
					else
						newY = myY + 1;
				}
				return (b.getFreeCount(newX, newY) >= 3);
			}
		}

		@Override
		public int getCategory() {
			return CATEGORY_TERRITORIAL;
		}
		
		@Override
		public String toString() {
			return "Territorial";
		}
	};
	
	public final static MoveCategory OBSTRUCTIVE = new MoveCategory() {
		@Override
		public boolean belongsTo(Board b, int playerNr, int m) {
			int myPos = b.getPlayerPosition(playerNr);
			int myX = Board.posToX(myPos), myY = Board.posToY(myPos);
			int newX = myX, newY = myY;
			if (m < 0) {
				if (m == Board.MOVE_LEFT)
					newX--;
				else
					newY--;
			}
			else {
				if (m == Board.MOVE_RIGHT)
					newX++;
				else
					newY++;
			}
			return b.isIsolatingMove(myX, myY, newX, newY);
		}

		@Override
		public int getCategory() {
			return CATEGORY_OBSTRUCTIVE;
		}
		
		@Override
		public String toString() {
			return "Obstructive";
		}
	};
	
	public final static MoveCategory RECKLESS = new MoveCategory() {
		@Override
		public boolean belongsTo(Board b, int playerNr, int m) {
			int myPos = b.getPlayerPosition(playerNr);
			int otherPos = b.getPlayerPosition(1 - playerNr);
			int myX = Board.posToX(myPos), myY = Board.posToY(myPos);
			int otherX = Board.posToX(otherPos), otherY = Board.posToY(otherPos);
			if (Board.getManhattanDistance(myX, myY, otherX, otherY) == 2) {
				if (m < 0) {
					if (m == Board.MOVE_LEFT)
						myX--;
					else
						myY--;
				}
				else {
					if (m == Board.MOVE_RIGHT)
						myX++;
					else
						myY++;
				}
				return (Board.getManhattanDistance(myX, myY, otherX, otherY) == 1);
			}
			else
				return false;
		}

		@Override
		public int getCategory() {
			return CATEGORY_RECKLESS;
		}
		
		@Override
		public String toString() {
			return "Reckless";
		}
	};
	
	private final static MoveCategory[] categories = new MoveCategory[] {PASSIVE, OFFENSIVE, DEFENSIVE, TERRITORIAL, OBSTRUCTIVE, RECKLESS};
	private Game game;
	private MoveStatistics stats;

	public GameAnalyzer() {
		this.stats = new MoveStatistics();
	}
	
	public GameAnalyzer(Game game) {
		this();
		setGame(game);
	}
	
	public GameAnalyzer(String statsFile) {
		this();
		try {
			this.stats = MoveStatistics.fromFile(statsFile);
		} catch (Exception e) {
			e.printStackTrace();
			throw new IllegalStateException("I really need this stats file '" + statsFile + "'");
		}
	}
	
	public GameAnalyzer(Game game, String statsFile) {
		this(game);
		try {
			this.stats = MoveStatistics.fromFile(statsFile);
		} catch (Exception e) {
			e.printStackTrace();
			throw new IllegalStateException("I really need this stats file '" + statsFile + "'");
		}
	}
	
	public void setGame(Game g) {
		if (this.game != null)
			this.game.removeGameListener(this);
		this.game = g;
		g.addGameListener(this);
	}
	
	public Game getGame() {
		return game;
	}
	
	@Override
	public void onMoveBegin(Game g) {
		if (game != g)
			throw new IllegalStateException("what is this madness, I do not know this game");
		Board b = game.board;
		for (int i = 0; i < 2; i++) {
			int pos = b.getPlayerPosition(i);
			int x = Board.posToX(pos), y = Board.posToY(pos);
			ArrayList<Integer> moves = b.getValidMoves(x, y);
			
			ArrayList<Integer> uniqueCats = new ArrayList<Integer>(4);
			HashMap<Integer, int[]> stats = this.stats.stats;
			for (int j = 0; j < moves.size(); j++) {
				int m = moves.get(j);
				int cat = getCategory(b, i, m);
				if (!uniqueCats.contains(cat)) {
					uniqueCats.add(cat);
					if (stats.containsKey(cat))
						stats.get(cat)[1]++;
					else
						stats.put(cat, new int[] {0, 1});
				}
			}
		}
	}
	
	@Override
	public void onPlayerMove(Game g, Board b, Player p, int m) {
		if (game != g)
			throw new IllegalStateException("what is this madness, I do not know this game");

		int category = getCategory(b, p.getNumber(), m);
		HashMap<Integer, int[]> catStats = stats.stats;
		int[] cats;
		if (catStats.containsKey(category)) {
			cats = catStats.get(category);
			cats[0]++;
		}
		else {
			cats = new int[] {1, 0};
			catStats.put(category, cats);
		}
	}
	
	public static int getCategory(Board b, int playerNr, int m) {
		int category = 0;
		for (int i = 0; i < categories.length; i++) {
			MoveCategory cat = categories[i];
			if (cat.belongsTo(b, playerNr, m)) {
				//System.out.println("Added cat " + cat);
				category += cat.getCategory();
			}
		}
		//System.out.println("Cat: " + getCategoryName(category));
		return category;
	}
	
	public static String getCategoryName(int cat) {
		StringBuilder builder = new StringBuilder("MoveCategory");
		for (int i = 0; i < categories.length; i++) {
			if ((cat & categories[i].getCategory()) == categories[i].getCategory())
				builder.append("." + categories[i]);
		}
		return builder.toString();
	}
	
	public MoveStatistics getStatistics() {
		return stats;
	}
	
	public void save(String file) throws Exception {
		stats.toFile(file);
	}
	
	@Override
	public void onStart(Game g) {}

	@Override
	public void onPause(Game g) {}

	@Override
	public void onResume(Game g) {}

	@Override
	public void onDraw(Game g, Player[] winning) {}

	@Override
	public void onWin(Game g, Player p) {}

	@Override
	public void onGameOver(Game g) {}

	@Override
	public void onPlayerDied(Game g, Player p) {}

	@Override
	public void onNextTurn(Game g) {}

	@Override
	public void onMoveEnd(Game g) {
		//stats.print();
	}
	
	public static class MoveStatistics implements Serializable {
		private static final long serialVersionUID = -7428160996380765775L;
		public HashMap<Integer, int[]> stats = new HashMap<Integer, int[]>();
		
		public double getTransitionalProbability(int category) {
			if (stats.containsKey(category)) {
				int[] dat = stats.get(category);
				return (dat[0] / (double) dat[1]);
			}
			else
				return 0;
		}
		
		public void print() {
			Set<Integer> cats = stats.keySet();
			System.out.println("Move statistics:");
			for (Integer cat : cats) {
				int[] catStats = stats.get(cat);
				System.out.println("  " + cat + ": [" + catStats[0] + " | " + catStats[1] + "] (" + getCategoryName(cat) + ")");
			}
		}
		
		/* Write to the given file */
		public void toFile(String file) throws Exception {
			File path = new File(file);
			if (path.getParent() != null) {
				File dir = new File(path.getParent());
				dir.mkdirs();
			}
			
			ObjectOutput ObjOut = new ObjectOutputStream(new FileOutputStream(path));
			ObjOut.writeObject(this);
			ObjOut.close();
		}
		
		public static MoveStatistics fromFile(String file) throws Exception {
			ObjectInput ObjInput = new ObjectInputStream(new FileInputStream(file));
			MoveStatistics stats = null;
			try {
				stats = (MoveStatistics) ObjInput.readObject();
			} catch (ClassNotFoundException e) {}
			finally {
				ObjInput.close();
			}
			return stats;
		}
		
		public void merge(MoveStatistics otherStats) {
			Set<Integer> cats = otherStats.stats.keySet();
			for (Integer cat : cats) {
				int[] catStats = otherStats.stats.get(cat);
				if (stats.containsKey(cat)) {
					int[] myStats = stats.get(cat);
					myStats[0] += catStats[0];
					myStats[1] += catStats[1];
				}
				else
					stats.put(cat, catStats);
			}
		}
	}
	
	public static void main(String[] args) {
		if (args.length == 0)
			return;
		boolean search = args[0].equals("-s");
		ArrayDeque<MoveStatistics> stats = new ArrayDeque<MoveStatistics>();
		for (int i = (search ? 1 : 0); i < args.length; i++) {
			String path = args[i];
			String[] files;
			if (search) {
				File dir = new File(path);
				files = dir.list(filter);
				for (int j = 0; j < files.length; j++)
					files[j] = path + File.separator + files[j];
			}
			else 
				files = new String[] {path};
			
			for (int j = 0; j < files.length; j++) {
				try {
					stats.add(MoveStatistics.fromFile(files[j]));
					//MoveStatistics.fromFile(files[j]).print();
					System.out.println("* Loaded '" + files[j] + "'");
				} catch (Exception e) {
					System.err.println("Unable to load '" + files[j] + "'");
					e.printStackTrace();
				}
			}
		}
		if (stats.isEmpty())
			return;
		
		System.out.println();
		MoveStatistics main = stats.poll();
		main.print();
		while (!stats.isEmpty()) {
			System.out.println("Merge main with: ");
			MoveStatistics s = stats.poll();
			s.print();
			main.merge(s);
		}
		main.print();
		try {
			main.toFile("blah.dat");
		} catch (Exception e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
	}
	
	private final static FilenameFilter filter = new FilenameFilter() {
		@Override
		public boolean accept(File dir, String name) {
			return name.endsWith(".dat");
		}
	};
}
